#!/usr/bin/python3

import os
import re
import json
import shutil
import hashlib
import filecmp
import subprocess


class Main:

    def __init__(self):
        # get mirrors plugin storage-file directory
        self.dstDir = "/var/lib/mirrors/sonyselect/storage-file"
        if not os.path.exists(self.dstDir):
            self.dstDir = "/var/cache/mirrors/sonyselect/storage-file"
            if not os.path.exists(self.dstDir):
                raise Exception("No sonyselect mirror directory found.")

        # get sha list
        self.shaDictFile = os.path.join(self.dstDir, ".sha")
        self.shaDict = dict()
        if os.path.exists(self.shaDictFile):
            with open(self.shaDictFile) as f:
                json.load(f)
        else:
            self._reCreateShaDictFile()

    def update(self):
        # check & fix mirrors plugin storage-file directory
        if True:
            bFound = False
            for tdir in os.listdir(self.dstDir):
                ddir = self._convertBasename(tdir)
                if ddir != tdir:
                    os.rename(os.path.join(self.dstDir, tdir), os.path.join(self.dstDir, ddir))
                    bFound = True
            if bFound:
                self._reCreateShaDictFile()

        # get candidate directory list
        candidateDirList = []
        for tdir in Util.getFileList(".", "d"):
            if tdir.startswith("."):
                continue
            fulldir = os.path.join(".", tdir)
            hasFile = len(Util.getFileList(fulldir, "f", 1)) > 0
            hasDir = len(Util.getFileList(fulldir, "d", 1)) > 0
            if hasFile and hasDir:
                raise Exception("Invalid directory \"%s\", it has both file and directory in it." % (fulldir))
            if not hasFile and not hasDir:
                raise Exception("Invalid directory \"%s\", it has nothing in it." % (fulldir))
            if hasDir and not hasFile:
                continue
            if hasFile and not hasDir:
                candidateDirList.append(tdir)
                continue
            assert False

        # process
        for tdir in candidateDirList:
            dstFullfn = None
            if True:
                basename = os.path.basename(tdir)
                if re.fullmatch("CD[0-9]+", basename):
                    basename = os.path.basename(os.path.dirname(tdir)) + " " + basename
                basename = self._convertBasename(basename)
                dstFullfn = os.path.join(self.dstDir, basename)
            
            if os.path.exists(dstFullfn):
                if not Util.compareDir(tdir, dstFullfn):
                    raise Exception("Directory \"%s\" and \"%s\" contain different content, needs manual intervention." % (tdir, dstFullfn))
                shutil.rmtree(tdir)
            else:
                # check
                for fn in os.listdir(tdir):
                    fullfn = os.path.join(tdir, fn)
                    sha = Util.shaFile(fullfn)
                    for k, v in self.shaDict.items():
                        if sha == v:
                            raise Exception("File \"%s\" and \"%s\" are same, needs manual intervention." % (fullfn, os.path.join(self.dstDir, k)))

                # move directory
                dstName = os.path.join(self.dstDir, os.path.basename(tdir))
                shutil.move(tdir, dstName)

                # add new files to shaDict (FIXME: low performance)
                for fn in Util.getFileList(self.dstDir, "f"):
                    if fn not in self.shaDict:
                        self.shaDict[fn] = Util.shaFile(os.path.join(self.dstDir, fn))
                with open(self.shaDictFile, "w") as f:
                    json.dump(self.shaDict, f)
            print("Processed \"%s\" -> \"%s\"." % (tdir, dstFullfn))

        # remove all empty directories
        for tdir in reversed(Util.getFileList(".", "d")):
            fullfn = os.path.join(".", tdir)
            if len(os.listdir(fullfn)) == 0:
                os.rmdir(fullfn)

        # change owner ship
        subprocess.run(["chown", "-R", "mirrors:mirrors", self.dstDir])

    def _convertBasename(self, src):
        redundantList = [
            "48.0kHz24bit",
            "44.1 16",
            "96 24",
            "48 24",
            "24 96",
            "24-48",
            "[]",
            "[ 24bit 48kHz]",
            "[24bit 48kHz]",
            "[24bit-96KHz]",
            "(24bit-96kHz flac)",
        ]

        ret = src
        for r in redundantList:
            ret = ret.replace(r, "")
        while ret.replace("  ", " ") != ret:
            ret = ret.replace("  ", " ")
        if True:
            ret = ret.strip(" ")
        return ret

    def _reCreateShaDictFile(self):
        for fn in Util.getFileList(self.dstDir, "f"):
            self.shaDict[fn] = Util.shaFile(os.path.join(self.dstDir, fn))
        with open(self.shaDictFile, "w") as f:
            json.dump(self.shaDict, f)


class Util:

    @staticmethod
    def shaFile(fullfn):
        h = hashlib.sha1()
        with open(fullfn, "rb") as f1:
            buf = f1.read(4096)
            while buf != b'':
                h.update(hashlib.sha1(buf).digest())
                buf = f1.read(4096)
        return h.hexdigest()

    @staticmethod
    def compareDir(dir1, dir2):
        ret = filecmp.dircmp(dir1, dir2)
        if len(ret.left_only) > 0 or len(ret.right_only) > 0:
            return False
        if len(ret.common_funny) > 0 or len(ret.diff_files) > 0 or len(ret.funny_files) > 0:
            return False

        ret = filecmp.cmpfiles(dir1, dir2, ret.common, shallow=False)
        if len(ret.mismatch) > 0 or len(ret.errors) > 0:
            return False

        return True

    @staticmethod
    def getFileList(dirName, typeList, level=None):
        """typeList is a string, value range is "d,f,l,a"
           returns basename"""

        ret = []
        for fbasename in os.listdir(dirName):
            fname = os.path.join(dirName, fbasename)

            # bleaf
            if os.path.isdir(fname):
                bleaf = True
                for x in os.listdir(fname):
                    fullx = os.path.join(fname, x)
                    if not os.path.islink(fullx) and os.path.isdir(fullx):
                        bleaf = False
                        break
            else:
                bleaf = True

            if not bleaf and (level is None or level - 1 > 0):
                for i in Util.getFileList(fname, typeList, level - 1 if level is not None else None):
                    ret.append(os.path.join(fbasename, i))
            else:
                if ("a" in typeList or "d" in typeList) and os.path.isdir(fname):           # directory
                    ret.append(fbasename)
                elif ("a" in typeList or "f" in typeList) and os.path.isfile(fname):        # file
                    ret.append(fbasename)
                elif ("a" in typeList or "l" in typeList) and os.path.islink(fname):        # soft-link
                    ret.append(fbasename)

        return ret


if __name__ == "__main__":
    if os.getuid() != 0:
        raise Exception("Only root user can run it.")
    Main().update()
